/*
 * Package il.ac.biu.cs.grossmm.impl.activeData
 * File NodeTypeImpl.java
 * Created on Jul 24, 2006
 *
 */
package il.ac.biu.cs.grossmm.api.data;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Defines a data node type based on node type interface. Instances of this
 * class are provided by {@link #nodeType(Class)} method. It is guaranteed that
 * there's at most one instance of this class per node type interface. Instances
 * of this class are lazily constructed when required and re-used when required
 * again.
 * <p>
 * This class also provides static methods to initialize constants in node type
 * interfaces.
 * 
 * @see NodeInterface
 * 
 * @author Mark Grossmann
 */
public class NodeTypeByInterface<N> implements NodeType<N> {

	/**
	 * Gets a node type defined by a node type interface. Since it is guaranteed
	 * that there's at most one instance of <tt>NodeTypeByInterface</tt> class
	 * per node type interface, two <tt>nodeType</tt> method invokations with
	 * the same argument will result in returning of the same instance of
	 * <tt>NodeTypeByInterface</tt>.
	 * 
	 * @param <N>
	 *            node type interface which defines the node type to construct
	 * @param clazz
	 *            a Class object of node type interface which defines the node
	 *            type to construct
	 * @return node type defined by given node type interface
	 * 
	 * @see NodeInterface
	 */
	@SuppressWarnings("unchecked")
	public static <N> NodeTypeByInterface<N> nodeType(Class<N> clazz)
	{
		lock.lock();
		try {

			NodeTypeByInterface<N> nodeType = nodeTypes.get(clazz);

			if (nodeType == null) {
				nodeType = new NodeTypeByInterface<N>(clazz);
				nodeTypes.put(clazz, nodeType);
			}

			return nodeType;

		} finally {
			lock.unlock();
		}
	}

	/**
	 * Constructs a value for property constant in node type interface
	 * 
	 * @param <N>
	 *            node type interface for which proeprty constant is constructed
	 * @param <V>
	 *            class of values which can be stored in this property
	 * @param nClass
	 *            Class object of ode type interface for which proeprty constant
	 *            is constructed
	 * @param vClass
	 *            Class object of value class
	 * 
	 * @see Property
	 * 
	 * @return a value for property constant in node type interface
	 */
	public static <N extends NodeInterface, V> Property<N, V> property(
		Class<N> nClass, Class<V> vClass)
	{
		return new PropertyByInterface<N, V>(nClass, vClass);
	}

	/**
	 * Constructs a value for unique property constant in node type interface
	 * 
	 * @param <N>
	 *            node type interface for which property constant is constructed
	 * @param <V>
	 *            class of values which can be stored in this property
	 * @param nClass
	 *            Class object of ode type interface for which proeprty constant
	 *            is constructed
	 * @param vClass
	 *            Class object of value class
	 * 
	 * @return a value for unique property constant in node type interface
	 * 
	 * @see Unique
	 */
	public static <N extends NodeInterface, V> Unique<N, V> unique(
		Class<N> nClass, Class<V> vClass)
	{
		return new UniqueByInterface<N, V>(nClass, vClass);
	}

	/**
	 * Constructs a value for node-set constant in a node type interface
	 * 
	 * @param <N>
	 *            node type interface for which proeprty constant is constructed
	 * @param <S>
	 *            node type interface which defines a type of nodes which can be
	 *            contained in this node-set
	 * @param nClass
	 *            Class object of node type interface for which proeprty
	 *            constant is constructed
	 * @param sClass
	 *            Class object of node type interface which defines a type of
	 *            nodes which can be contained in this node-set
	 * @return a value for node-set constant in a node type interface
	 */
	public static <N extends NodeInterface, S extends NodeInterface> NodesetByInterface<N, S> nodeset(
		Class<N> nClass, Class<S> sClass)
	{
		return new NodesetByInterface<N, S>(nClass, sClass);
	}

	/**
	 * Set of all known node types indexed by type interface Class objects
	 */
	private static Map<Class, NodeTypeByInterface> nodeTypes =
		new HashMap<Class, NodeTypeByInterface>();

	// //// fields

	private static Lock lock = new ReentrantLock();

	private final NodesetByInterface[] nodesets;

	private final PropertyByInterface[] properties;

	private final Class clazz;

	// //// non-static methods

	@SuppressWarnings("unchecked")
	private NodeTypeByInterface(Class clazz)
	{
		this.clazz = clazz;

		Field[] fields = clazz.getFields();

		List<NodesetByInterface> sets = new ArrayList<NodesetByInterface>();
		List<NodeTypeByInterface> nestedTypes =
			new ArrayList<NodeTypeByInterface>();
		List<PropertyByInterface> props = new ArrayList<PropertyByInterface>();

		List<String> propNames = new ArrayList<String>();
		List<String> nsetNames = new ArrayList<String>();

		for (int i = 0; i < fields.length; i++) {
			Field f = fields[i];

			// skip non static fields
			if ((f.getModifiers() & Modifier.STATIC) == 0)
				continue;

			if (Nodeset.class.isAssignableFrom(f.getType())) {

				// A NodeSet field
				NodesetByInterface nodeSet;
				try {
					nodeSet = (NodesetByInterface) f.get(null);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}

				Class declaringInterface = nodeSet.getDeclaringInterface();

				if (!declaringInterface.equals(clazz)) {
					throw new RuntimeException("Node-set " + f.getName()
						+ " is contained in " + clazz
						+ " but declared as if contained in "
						+ declaringInterface);
				}

				Class nestedInterface = nodeSet.getNestedInterface();

				NodeTypeByInterface nestedType = nodeType(nestedInterface);

				nsetNames.add(f.getName());
				sets.add(nodeSet);
				nestedTypes.add(nestedType);

			} else if (Property.class.isAssignableFrom(f.getType())) {

				// A property field
				PropertyByInterface property;
				try {
					property = (PropertyByInterface) f.get(null);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}

				if (!property.getDeclaringInterface().equals(clazz)) {
					throw new RuntimeException("Property " + f.getName()
						+ " is contained in " + clazz
						+ " but declared as if contained in "
						+ property.getDeclaringInterface());
				}

				int index = props.size();

				property.initialize(index, f.getName(), this);

				propNames.add(f.getName());
				props.add(property);

			}
		}

		properties = props.toArray(new PropertyByInterface[props.size()]);
		nodesets = sets.toArray(new NodesetByInterface[sets.size()]);

//		for (int i = 0; i < properties.length; i++) {
//			properties[i].initialize(i, this);
//		}

		for (int i = 0; i < nodesets.length; i++) {
			nodesets[i].initialize(i, nsetNames.get(i), this,
				nestedTypes.get(i));
		}
	}

	/**
	 * Returns node Class object of type interface used to construct this node
	 * type
	 * 
	 * @return node Class object of type interface used to construct this node
	 *         type
	 */
	public Class getNodeInterface()
	{
		return clazz;
	}

	/**
	 * Reserved for a future versions of API. Not used in this version.
	 * 
	 * @param n
	 * @return for a future versions of API. Not used in this version.
	 */
	protected boolean isAssignableFrom(NodeTypeByInterface n)
	{
		if (!(n instanceof NodeTypeByInterface))
			return false;

		// no generalization allowed at this stage
		return clazz.equals(((NodeTypeByInterface) n).clazz);
	}

	@Override
	public boolean equals(Object obj)
	{
		if (obj != null && obj instanceof NodeTypeByInterface) {
			NodeTypeByInterface n = (NodeTypeByInterface) obj;

			return n.clazz.equals(clazz);
		} else
			return false;
	}

	@Override
	public int hashCode()
	{
		return clazz.hashCode() ^ 0x6a6ed00d;
	}

	@Override
	public String toString()
	{
		return "node type by interface " + clazz.getSimpleName();
	}

	public Nodeset nodeset(int index)
	{
		return nodesets[index];
	}

	public Property property(int index)
	{
		return properties[index];
	}

	public int nodesetsNumber()
	{
		return nodesets.length;
	}

	public int propertiesNumber()
	{
		return properties.length;
	}

	/**
	 * Used to lazily construct instances of this class just before they are
	 * required
	 * 
	 * @param <N>
	 *            node type interface
	 * @param clazz
	 *            Class object of node type interface
	 */
	static <N> void initialize(Class<N> clazz)
	{
		nodeType(clazz);
	}

}
